package edu.chalmers.AV_2DPlatformerFramework.Model;

import edu.chalmers.AV_2DPlatformerFramework.Utility.Pair;
import edu.chalmers.AV_2DPlatformerFramework.Utility.Vector2D;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

/**
 * Representation of object in game world.
 * Has a position, velocity, weight and elasticity as well as a physical
 * {@code Hitbox} with a corresponding interaction {@code Layer}. It is
 * represented visually by one or multiple {@code Texture}s.
 * @version 0.42 // What happened with the 36 versions 0.5 through 0.40?
 * @author Richard
 * @revised Richard, 04-04-2014: Refactoring to use the Texture class.
 * @revised Joakim Karlsson & Gustav Vidhög, 20140407
 * @revised Gustav Vidhög, 20140417
 * @revised Andrea Buchholz & Richard Sundqvist, 080514.
 * @revised Richard Sundqvist, 2014-05-09: Implemented equals method.
 * @revised Gustav Vidhög, 20140514
 * @Revised Richard Sundqvist Added strata variable to determine draw order.
 */
public abstract class GameObject {
    /**
     * Position of this object.
     * Expressed as a vector in game coordinate space.
     */
    private Vector2D position;
    
    /**
     * Velocity of this object.
     * Expressed as a vector in game coordinate space.
     */
    private Vector2D velocity;
    
    /**
     * {@code Hitbox} of this object.
     * Used to describe the physical shape of this object, relative to its
     * position, for detecting and handling collisions with other objects.
     */
    private Hitbox hitbox;
    
    /**
     * {@code Layer} of this object.
     * Used to determine which objects this object interacts with physically.
     */
    private Layer layer;
    
    /**
     * Object elasticity.
     * Used to calculate elasticity of collisions with this object.
     */
    private double elasticity;
    
    /**
     * Relative object weight.
     * Used to calculate the new speeds after collisions with this object.
     * Either takes a nominal {@code Double} value if the course of the object
     * can be altered by other objects, or the value {@code
     * Double.POSITIVE_INFINITY} if it can not.
     */
    private double weight;
    
    /**
     * The {@code Texture}(s) for this {@code GameObject}.
     */
    protected final ArrayList<Texture> TEXTURES;
    
    /**
     * The Rectangle2D in which this GameObjects Texture is painted.
     */
    private Rectangle2D.Double drawingArea;

    /**
     * The index of the Texture currently used by this GameObject.
     */
    private int currentTextureIndex = 0;
    
    /**
     * Determines in which order this GameObject will be drawn. Lower strata objects will be drawn behind higher strata ones.
     */
    private int strata;
    
    //Constructors
    
    //TODO Make constructors make sense for our application.
    
    /**
     * Constructs a {@code GameObject} with specified {@code Hitbox}. The new
     * object interacts with all other objects capable of physical interaction,
     * is at rest positioned at the origin, has a weight and an elasticity of
     * unity, and has no textures.
     * @param hitbox - physical representation of new {@code GameObject}.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     * @revised Gustav Vidhög, 20140514
     * @deprecated
     */
    @Deprecated
    public GameObject(Hitbox hitbox) {
        this(hitbox, new UniversalInteractionLayer(), 1, 1);
    }
    
    /**
     * Constructs a {@code GameObject} with {@code Hitbox} specified by
     * Polygon-style parameters. The new object interacts with all other objects
     * capable of physical interaction, is at rest positioned at the origin, has
     * a weight and an elasticity of unity, and has no textures.
     * @param xCoords - array of X coordinates.
     * @param yCoords - array of Y coordinates.
     * @param nbrOfPoints - number of vertices in {@code Hitbox} of new
     * {@code GameObject}.
     * @throws NegativeArraySizeException if {@code nbrOfPoints} is negative.
     * @throws IndexOutOfBoundsException if there are an insufficient number of
     * coordinates in {@code xCoords} or {@code yCoords} to build a
     * {@code Hitbox} with {@code nbrOfPoints} number of vertices.
     * @throws Hitbox.HitboxMalformedException if {@code Hitbox} of new
     * {@code GameObject} is not well-formed.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.1
     * @revised Gustav Vidhög, 20140514
     * @deprecated
     */
    @Deprecated
    public GameObject(int[] xCoords, int[]yCoords, int nbrOfPoints)
            throws Hitbox.HitboxMalformedException {
        this(new Hitbox(xCoords, yCoords, nbrOfPoints));
    }
    
    /**
     * Constructs a {@code GameObject} with specified {@code Hitbox},
     * {@code Layer}, elasticity and weight. The new object is at rest
     * positioned at the origin and has no textures.
     * @param hitbox - physical representation of new {@code GameObject}.
     * @param layer - measure of which objects new {@code GameObject} will
     * interact with physically.
     * @param elasticity - elasticity of new {@code GameObject}.
     * @param weight - weight of new {@code GameObject}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public GameObject(Hitbox hitbox, Layer layer,
            double elasticity, double weight) {
        this.hitbox = hitbox;
        this.layer = layer;
        this.elasticity = elasticity;
        this.weight = weight;
        this.position = new Vector2D();
        this.velocity = new Vector2D();
        this.TEXTURES = new ArrayList<>();
    }
    
    /**
     * Constructs a {@code GameObject} with specified {@code Hitbox},
     * {@code Layer}, elasticity, weight, and {@code Texture}(s). The new object
     * is at rest positioned at the origin.
     * @param hitbox - physical representation of new {@code GameObject}.
     * @param layer - measure of which objects new {@code GameObject} will
     * interact with physically.
     * @param elasticity - elasticity of new {@code GameObject}.
     * @param weight - weight of new {@code GameObject}.
     * @param textures - visual representation of new {@code GameObject}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public GameObject(Hitbox hitbox, Layer layer,
            double elasticity, double weight,
            ArrayList<Texture> textures) {
        this(hitbox, layer, elasticity, weight);
        TEXTURES.addAll(textures);
        updateDrawingArea();
    }
    
    /**
     * Constructs a {@code GameObject} with specified {@code Hitbox},
     * {@code Layer}, elasticity, weight and velocity, at the specified
     * position. The new object has no textures.
     * @param hitbox - physical representation of new {@code GameObject}.
     * @param layer - measure of which objects new {@code GameObject} will
     * interact with physically.
     * @param elasticity - elasticity of new {@code GameObject}.
     * @param weight - weight of new {@code GameObject}.
     * @param position - position of new {@code GameObject}.
     * @param velocity - velocity of new {@code GameObject}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public GameObject(Hitbox hitbox, Layer layer,
            double elasticity, double weight,
            Vector2D position, Vector2D velocity) {
        this(hitbox, layer, elasticity, weight);
        this.position = position;
        this.velocity = velocity;
        
    }
    
    /**
     * Constructs a {@code GameObject} with specified {@code Hitbox},
     * {@code Layer}, elasticity, weight, velocity, and {@code Texture}(s), at
     * the specified position.
     * @param hitbox - physical representation of new {@code GameObject}.
     * @param layer - measure of which objects new {@code GameObject} will
     * interact with physically.
     * @param elasticity - elasticity of new {@code GameObject}.
     * @param weight - weight of new {@code GameObject}.
     * @param position - position of new {@code GameObject}.
     * @param velocity - velocity of new {@code GameObject}.
     * @param textures - visual representation of new {@code GameObject}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public GameObject(Hitbox hitbox, Layer layer,
            double elasticity, double weight,
            Vector2D position, Vector2D velocity,
            ArrayList<Texture> textures) {
        this(hitbox, layer, elasticity, weight, position, velocity);
        TEXTURES.addAll(textures);
        updateDrawingArea();
    };
    
    //onCollision
    
    public abstract void onCollision(GameObject otherObject);
    
    //Accessors and mutators
    
    /**
     * Sets position of object.
     * @param newPosition - new position of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public void setPosition(Vector2D newPosition) {
        this.position = newPosition;
    }
    
    /**
     * Gets position of object.
     * @return position of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public Vector2D getPosition() {
        return this.position;
    }
    
    /**
     * Sets velocity of object.
     * @param newVelocity - new velocity of object.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.1
     */
    public void setVelocity(Vector2D newVelocity) {
        this.velocity = newVelocity;
    }
    
    /**
     * Gets velocity of object.
     * @return velocity of object.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.1
     */
    public Vector2D getVelocity() {
        return this.velocity;
    }

    /**
     * Sets {@code Hitbox} of object.
     * @param newHitbox - new {@code Hitbox} of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public void setHitbox(Hitbox newHitbox) {
        this.hitbox = newHitbox;
    }
    
    /**
     * Gets {@code Hitbox} of object.
     * @return {@code Hitbox} of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public Hitbox getHitbox() {
        return this.hitbox;
    }
    
    /**
     * Sets {@code Layer} of object.
     * @param newLayer - new {@code Layer} of object.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public void setLayer(Layer newLayer) {
        this.layer = newLayer;
    }
    
    /**
     * Gets {@code Layer} of object.
     * @return {@code Layer} of object.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public Layer getLayer() {
        return this.layer;
    }
    
    /**
     * Sets elasticity of object.
     * @param newElasticity - new elasticity of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public void setElasticity(double newElasticity) {
        this.elasticity = newElasticity;
    }
    
    /**
     * Gets elasticity of object.
     * @return elasticity of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public double getElasticity() {
        return this.elasticity;
    }
    
    /**
     * Sets weight of object.
     * @param newWeight - new weight of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public void setWeight(double newWeight) {
        this.weight = newWeight;
    }
    
    /**
     * Gets weight of object.
     * @return weight of object.
     * @author Joakim Karlsson & Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public double getWeight() {
        return this.weight;
    }
    
    /**
     * Method specifying whether this {@code GameObject} interacts physically
     * with another.
     * @param otherObject - the {@code GameObject} to test whether this
     * {@code GameObject} interacts with.
     * @return {@code true} if this {@code GameObject} interacts physically with
     * {@code otherObject}; {@code false} otherwise.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    public boolean interactsWith (GameObject otherObject) {
        return layer.interactsWith(otherObject.getLayer());
    }
    
    /**
     * Creates and returns a deep copy of this {@code GameObject}. For any
     * {@code GameObject} {@code g}, the expression:
     * <blockquote>
     * <pre>
     * g.copy() != g</pre></blockquote>
     * and the expression:
     * <blockquote>
     * <pre>
     * x.copy().equals(x)</pre></blockquote>
     * are both {@code true}.
     * @return a deep copy of this {@code GameObject}.
     * @author Andrea Buchholz & Richard Sundqvist
     * @version 1.1
     * @since 0.41
     * @revised Gustav Vidhög, 20140514
     */
    public abstract GameObject copy();
    
    /**
     * Indicates whether some other object is "equal to" this {@code
     * GameObject}. Two {@code GameObject}s are equal if their {@code Hitbox},
     * {@code Layer}, weight, elasticity, and first {@code Texture} are equal.
     * Does NOT take into account position and velocity.
     * @param obj - the reference {@code Object} with which to compare.
     * @return {@code true} if this {@code GameObject} is the same as the
     * {@code obj} argument; {@code false} otherwise.
     * @author Richard Sundqvist
     * @version 1.1
     * @since 0.41
     * @revised Gustav Vidhög, 20140514
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof GameObject))
            return false;
        if (obj == this)
            return true;
        GameObject rhs = (GameObject) obj;
        
        return (this.hitbox.equals(rhs.hitbox)
                && this.layer.equals(rhs.layer)
                && this.weight == rhs.weight
                && this.elasticity == rhs.elasticity
                && this.getTexture(0) == rhs.getTexture(0));
                /*
                Questionable comparison, but any two GameObjects of the share
                type should share the same textures.
                WHY NOT COMPARE ALL TEXTURES?
                */
    }
    
    /**
     * Returns a hash code value for the {@code GameObject}.
     * @return a hash code value for this {@code GameObject}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.42
     */
    @Override
    public int hashCode() {
        long tempWeight = Double.doubleToLongBits(weight);
        long tempElasticity = Double.doubleToLongBits(elasticity);
        return (((int) (tempWeight ^ (tempWeight >>> 32))) * 71 +
                ((int) (tempElasticity ^ (tempElasticity >>> 32))) * 73 +
                this.hitbox.hashCode() * 79 +
                this.layer.hashCode() * 83 +
                this.getTexture(0).hashCode() * 89);
                //20th through 24th primes.
        /*
        Only using first Texture to be consistent with equals.
        Also relies on hashCode in Object for that same reason.
        */
    }
    
    /**
     * Creates a {@code Texture} to use for this {@code GameObject}, and adds it
     * to the array of possible skins. Can be fetched static or dynamic texture.
     * @param texture - {@code BufferedImage} to use as {@code Texture}.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.2
     */
    public void addTexture(Texture texture) {
        if (!(texture instanceof Texture)) // This is just a null test, right?
            throw new IllegalArgumentException("Not a Texture.");
        TEXTURES.add(texture);
    }
    /**
     * Returns an updated {@code Texture} based on this {@code GameObject}s
     * current location.
     * @param index - index of the {@code Texture} to fetch.
     * @return the {@code TexturePaint} with translation applied.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.2
     */
    public Texture getTexture(int index) {
        return (Texture)TEXTURES.get(index);
    }
    
    /**
     * Returns an (@code ArrayList) with all the (@code Textures)s associated
     * with this GameObject.
     * @return all of the objects textures if any, null otherwise.
     */
    public ArrayList<Texture> getTextures() {
        return TEXTURES;
    }
    
    /**
     * Returns the texture at index currentTextureIndex in TEXTURES.
     * @return The texture currently in use by this GameObject.
     * @author Richard Sundqvist
     * @version 1.0
     */
    public Texture getCurrentTexture(){
        return TEXTURES.get(currentTextureIndex);
    }
    /**
     * Change the currentTextureIndex of this GameObject,
     * changing which Texture is used by default.
     * @param newIndex The new index.
     */
    public void setCurrentTexture(int newIndex){
        if (newIndex < 0 || newIndex > TEXTURES.size()-1)
            throw new IndexOutOfBoundsException();
        currentTextureIndex = newIndex;
    }
    /**
     * Returns the Texture and Rectangle2D to use as the drawing area when drawing this GameObject.
     * @return A pair containing the current Texture and drawing area for this GameObject.
     * @Author Richard Sundqvist
     * @version 0.1
     */
    public Pair<Texture, Shape> getVisual(){
        updateDrawingArea();
        return new Pair(TEXTURES.get(currentTextureIndex).getUpdatedTexture(drawingArea), drawingArea);
    }
    
    /**
     * Returns the visual representation of the Hitbox as a Polygon.
     * @return The GameObjects Hitbox, using the same anchor point as the GameObject.
     * @author Richard Sundqvist
     * @version 1.0
     */
    public Polygon getHitboxVisual(){
        Vector2D[] vertices = hitbox.getVertices();
        
        int[] x = new int[vertices.length];
        int[] y = new int[vertices.length];
        
        for(int i = 0; i < vertices.length; i++){
            x[i] = (int)(vertices[i].x + position.x + 0.5);
            y[i] = (int)(vertices[i].y + position.y + 0.5);
        }
        return new Polygon(x, y, x.length);    
    }
    
    public void setStrata(int newStrata){
        this.strata = newStrata;
    }
    public int getStrata(){
        return this.strata;
    }
    
    
    /**
     * Allows this object to draw itself using the given {@code Graphics2D}
     * context.
     * @param g2d - graphics context to draw on.
     * @param textureIndex - index of the texture to use.
     * @author Richard Sundqvist
     * @version 1.0
     * @since 0.2
     */
    public void drawMe(Graphics2D g2d, int textureIndex) {
        Texture t = (Texture)TEXTURES.get(textureIndex);
        t.drawMe(g2d, drawingArea);
    }
    
    /**
     * Draws this {@code GameObject} on the specified {@code Graphics2D} context
     * using the {@code Texture} at index 0.
     * @param g2d - graphics context to draw on.
     * @author Richard Sundqvist
     * @version 1.1
     * @since 0.2
     * @revised Gustav Vidhög 20140514
     */
    public void drawMe(Graphics2D g2d) {
        drawMe(g2d, currentTextureIndex);
    }

    /**
     * Updates the Rectangle2D which is used when drawing this GameObject.
     * @author Richard Sundqvist
     * @version 1.0
     */
    public final void updateDrawingArea(){
        drawingArea = new Rectangle2D.Double(
                position.getX(),
                position.getY(),
                (double)TEXTURES.get(0).getImage().getWidth(),
                (double)TEXTURES.get(0).getImage().getHeight()
                );
//        drawingArea.add(position);
    }
    
    /**
     * Copies variables' values from this object to the copy.
     * Values from this will be copied to copy. Intended for use by implemented subclasses' copy() method.
     * Copies:
     * elasticity
     * layer
     * position
     * velocity
     * weight
     * @param copy The target object.
     * @author Richard Sundqvist
     * @version 1.0
     */
    public void copyVariablesTo(GameObject copy){
        copy.elasticity = this.elasticity;
        copy.layer = this.layer.copy();
        copy.position = this.position.copy();
        copy.velocity = this.velocity.copy();
        copy.weight = this.weight;
        
        for (Texture t : this.TEXTURES){
            copy.TEXTURES.add(t.copy());
        }
    }
}
